/* $Id: DatabaseInputStream.java 63 2010-02-14 13:52:34Z aaron.day $ */
package org.crazydays.gameplan.db.io;


import java.beans.XMLDecoder;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.imageio.ImageIO;

import org.apache.commons.beanutils.BeanUtils;
import org.crazydays.gameplan.db.Database;
import org.crazydays.gameplan.map.GameMap;
import org.crazydays.gameplan.map.Geometry;
import org.crazydays.gameplan.map.location.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DatabaseInputStream
 */
public class DatabaseInputStream
    extends FilterInputStream
{
    /** logger */
    protected static Logger LOGGER =
        LoggerFactory.getLogger(DatabaseInputStream.class);

    /** game map pattern */
    protected Pattern gameMapPattern = Pattern.compile("gameMaps/(.*?)/(.*)");

    /**
     * DatabaseInputStream constructor.
     * 
     * @param stream Stream
     */
    public DatabaseInputStream(InputStream stream)
    {
        super(new ZipInputStream(stream));
    }

    /**
     * Get stream as ZipInputStream.
     * 
     * @return ZipInputStream
     */
    private ZipInputStream stream()
    {
        return (ZipInputStream) in;
    }

    /**
     * Read database.
     * 
     * @return Database
     * @throws IOException
     */
    public Database readDatabase()
        throws IOException
    {
        Database database = new Database();

        marshal(database);

        return database;
    }

    /**
     * Marshal the database entries to the appropriate read method.
     * 
     * @param database Database
     * @throws IOException
     */
    protected void marshal(Database database)
        throws IOException
    {
        ZipEntry entry;
        while ((entry = stream().getNextEntry()) != null) {
            if (entry.getName().equals("properties.xml")) {
                readProperties(database);
            }
            if (entry.getName().startsWith("gameMaps")) {
                readGameMap(database, entry);
            }
        }
    }

    /**
     * Read object from the stream.
     * 
     * @return Object
     */
    protected Object readObject()
    {
        XMLDecoder decoder =
            new XMLDecoder(new XMLDecoderInputStreamWrapper(stream()));

        try {
            return decoder.readObject();
        } finally {
            decoder.close();
        }
    }

    /**
     * Read database properties.
     * 
     * @param database Database
     */
    protected void readProperties(Database database)
    {
        @SuppressWarnings("unchecked")
        Map<String, Object> properties = (Map<String, Object>) readObject();

        applyProperties(database, properties);
    }

    /**
     * Apply properties to database.
     * 
     * @param database Database
     * @param properties Properties
     */
    protected void applyProperties(Database database,
        Map<String, Object> properties)
    {
        applyProperty(database, properties, "name");
    }

    /**
     * Apply property to database.
     * 
     * @param database Database
     * @param properties Properties
     * @param property Property
     */
    protected void applyProperty(Database database,
        Map<String, Object> properties, String property)
    {
        if (database == null) {
            throw new IllegalArgumentException("database == null");
        }
        if (properties == null) {
            throw new IllegalArgumentException("properties == null");
        }
        if (property == null) {
            throw new IllegalArgumentException("property == null");
        }

        if (properties.containsKey(property)) {
            Object value = properties.get(property);

            try {
                BeanUtils.copyProperty(database, property, value);
            } catch (IllegalAccessException e) {
                LOGGER.warn("Unable to set property {0} to {1}", property,
                    value);
            } catch (InvocationTargetException e) {
                LOGGER.warn("Unable to set property {0} to {1}", property,
                    value);
            }
        }
    }

    /**
     * Read game maps into database.
     * 
     * @param database Database
     * @param entry Entry
     * @throws IOException
     */
    protected void readGameMap(Database database, ZipEntry entry)
        throws IOException
    {
        if (database == null) {
            throw new IllegalArgumentException("database == null");
        }
        if (entry == null) {
            throw new IllegalArgumentException("entry == null");
        }

        Matcher matcher = gameMapPattern.matcher(entry.getName());
        if (!matcher.matches()) {
            LOGGER.warn("gameMap entry {0} does not match {1}",
                entry.getName(), gameMapPattern.pattern());
            return;
        }

        String mapName = matcher.group(1);
        String element = matcher.group(2);

        GameMap gameMap = database.getGameMap(mapName);
        if (gameMap == null) {
            gameMap = new GameMap(mapName);
            database.addGameMap(gameMap);
        }

        marshalGameMap(gameMap, element);
    }

    /**
     * Marshal game map element to the correct reader.
     * 
     * @param gameMap GameMap
     * @param element Element
     * @throws IOException
     */
    protected void marshalGameMap(GameMap gameMap, String element)
        throws IOException
    {
        if (element == null) {
            throw new IllegalArgumentException("element == null");
        }

        if (element.equals("image.png")) {
            readGameMapImage(gameMap);
        } else if (element.equals("geometry.xml")) {
            readGameMapGeometry(gameMap);
        } else if (element.equals("locations.xml")) {
            readGameMapLocations(gameMap);
        } else {
            LOGGER.warn("Unknown game map element {0}", element);
        }
    }

    /**
     * Read game map image.
     * 
     * @param gameMap GameMap
     * @throws IOException
     */
    protected void readGameMapImage(GameMap gameMap)
        throws IOException
    {
        if (gameMap == null) {
            throw new IllegalArgumentException("gameMap == null");
        }

        gameMap.setImage(ImageIO.read(stream()));
    }

    /**
     * Read game map geometry.
     * 
     * @param gameMap GameMap
     */
    protected void readGameMapGeometry(GameMap gameMap)
    {
        if (gameMap == null) {
            throw new IllegalArgumentException("gameMap == null");
        }

        Geometry geometry = (Geometry) readObject();
        gameMap.setGeometry(geometry);
    }

    /**
     * Read game map locations.
     * 
     * @param gameMap GameMap
     */
    protected void readGameMapLocations(GameMap gameMap)
    {
        if (gameMap == null) {
            throw new IllegalArgumentException("gameMap == null");
        }

        @SuppressWarnings("unchecked")
        Set<Location> locations = (Set<Location>) readObject();
        gameMap.setLocations(locations);
    }
}
